import os
import subprocess
import requests
import json
import argparse
from pathlib import Path
from urllib.parse import urljoin

class GogsBackupTool:
    def __init__(self, gogs_url, token, source_dir, dry_run=False):
        self.gogs_url = gogs_url.rstrip('/')
        self.token = token
        self.source_dir = Path(source_dir).expanduser().resolve()
        self.dry_run = dry_run
        self.headers = {"Authorization": f"token {token}"}
        self.gogs_api_base = urljoin(self.gogs_url, '/api/v1/')
        self.gogs_username = self.get_gogs_username()  # 获取Gogs用户名

    def get_gogs_username(self):
        """获取Gogs用户名"""
        url = urljoin(self.gogs_api_base, "user")
        response = requests.get(url, headers=self.headers)
        if response.status_code == 200:
            return response.json()["username"]
        else:
            print(f"警告: 无法获取Gogs用户名，使用默认值 'your_username'")
            return "your_username"

    def run(self):
        if not self.source_dir.is_dir():
            print(f"错误: 源目录不存在: {self.source_dir}")
            return

        git_repos = self.find_git_repositories()
        if not git_repos:
            print("未在指定目录下找到Git仓库")
            return

        print(f"找到 {len(git_repos)} 个Git仓库")
        for repo_path in git_repos:
            self.process_repository(repo_path)

    def find_git_repositories(self):
        """递归查找目录下的所有Git仓库"""
        git_repos = []
        for root, dirs, _ in os.walk(self.source_dir):
            if '.git' in dirs:
                git_repos.append(Path(root))
                # 避免进入子仓库的.git目录
                dirs[:] = [d for d in dirs if d != '.git']
        return git_repos

    def process_repository(self, repo_path):
        """处理单个仓库：检查、创建并推送"""
        repo_name = repo_path.name
        repo_relative_path = repo_path.relative_to(self.source_dir)
        print(f"\n处理仓库: {repo_relative_path}")

        # 获取仓库描述（如果有README）
        readme_path = repo_path / 'README.md'
        description = ""
        if readme_path.exists():
            with open(readme_path, 'r', encoding='utf-8') as f:
                description = f.read().splitlines()[0][:100]  # 取第一行的前100个字符

        # 检查Gogs上是否已存在该仓库
        if self.repo_exists(repo_name):
            print(f"仓库 '{repo_name}' 已存在，将更新")
            self.update_repository(repo_path, repo_name)
        else:
            print(f"仓库 '{repo_name}' 不存在，将创建")
            self.create_and_push_repository(repo_path, repo_name, description)

    def repo_exists(self, repo_name):
        """检查Gogs上是否存在指定名称的仓库"""
        url = urljoin(self.gogs_api_base, f"user/repos")
        response = requests.get(url, headers=self.headers)

        if response.status_code != 200:
            print(f"错误: 获取仓库列表失败 ({response.status_code}): {response.text}")
            return False

        repos = response.json()
        return any(repo['name'] == repo_name for repo in repos)

    def create_and_push_repository(self, repo_path, repo_name, description, current_branch=None):
        """创建新仓库并推送代码"""
        if self.dry_run:
            print(f"[干跑模式] 会创建仓库: {repo_name}")
            print(f"[干跑模式] 会推送到: {self.get_gogs_repo_url(repo_name)}")
            return

        # 创建仓库
        url = urljoin(self.gogs_api_base, "user/repos")
        data = {
            "name": repo_name,
            "description": description,
            "private": True,
            "auto_init": False
        }
        response = requests.post(url, headers=self.headers, json=data)

        if response.status_code in (200, 201):
            print(f"成功创建仓库: {repo_name}")
            if current_branch is None:
                current_branch = self.run_git_command(repo_path, ["rev-parse", "--abbrev-ref", "HEAD"]).strip()
                if not current_branch:
                    # 获取 commit id 并创建 undefined-branch 分支
                    commit_id = self.run_git_command(repo_path, ["rev-parse", "HEAD"]).strip()
                    if commit_id:
                        new_branch_name = f"undefined-branch-{commit_id[:7]}"
                        self.run_git_command(repo_path, ["checkout", "-b", new_branch_name])
                        current_branch = new_branch_name
                        print(f"为仓库 {repo_name} 创建并切换到 {new_branch_name} 分支")
                    else:
                        print("错误: 无法获取当前分支名和 commit id，跳过推送操作")
                        return
            self.push_both_branches(repo_path, repo_name, current_branch)
        else:
            print(f"错误: 创建仓库失败 ({response.status_code}): {response.text}")

    def update_repository(self, repo_path, repo_name, current_branch=None):
        """更新现有仓库"""
        if self.dry_run:
            print(f"[干跑模式] 会更新仓库: {repo_name}")
            return

        if current_branch is None:
            current_branch = self.run_git_command(repo_path, ["rev-parse", "--abbrev-ref", "HEAD"]).strip()
        self.push_both_branches(repo_path, repo_name, current_branch)

    def run_git_command(self, repo_path, command):
        """在指定仓库路径执行Git命令"""
        try:
            result = subprocess.run(
                ["git"] + command,
                cwd=repo_path,
                capture_output=True,
                text=True,
                encoding='utf-8',
                check=True
            )
            return result.stdout
        except subprocess.CalledProcessError as e:
            if "fatal: detected dubious ownership in repository" in e.stderr:
                try:
                    # 添加仓库目录到安全目录列表
                    subprocess.run(
                        ["git", "config", "--global", "--add", "safe.directory", str(repo_path)],
                        check=True
                    )
                    # 再次尝试执行原命令
                    result = subprocess.run(
                        ["git"] + command,
                        cwd=repo_path,
                        capture_output=True,
                        text=True,
                        encoding='utf-8',
                        check=True
                    )
                    return result.stdout
                except subprocess.CalledProcessError as retry_e:
                    print(f"尝试添加安全目录后，执行 git 命令 {' '.join(['git'] + command)} 仍然失败，工作目录: {repo_path}")
                    print(f"标准输出: {retry_e.stdout}")
                    print(f"标准错误: {retry_e.stderr}")
                    raise retry_e
            print(f"执行 git 命令 {' '.join(['git'] + command)} 失败，工作目录: {repo_path}")
            print(f"标准输出: {e.stdout}")
            print(f"标准错误: {e.stderr}")
            raise e

    def push_both_branches(self, repo_path, repo_name, current_branch):
        """推送当前分支和以 gogs/ 为前缀的分支到 Gogs"""
        gogs_remote = self.get_gogs_repo_url(repo_name)
        try:
            # 确保 current_branch 不为空
            if not current_branch:
                current_branch = self.run_git_command(repo_path, ["rev-parse", "--abbrev-ref", "HEAD"]).strip()
                if not current_branch:
                    # 获取 commit id 并创建 undefined-branch 分支
                    commit_id = self.run_git_command(repo_path, ["rev-parse", "HEAD"]).strip()
                    if commit_id:
                        new_branch_name = f"undefined-branch-{commit_id[:7]}"
                        self.run_git_command(repo_path, ["checkout", "-b", new_branch_name])
                        current_branch = new_branch_name
                        print(f"为仓库 {repo_name} 创建并切换到 {new_branch_name} 分支")
                    else:
                        print("错误: 无法获取当前分支名和 commit id，终止操作")
                        return

            # 构造 gogs 分支名，确保只有一个 /
            gogs_branch = f"{current_branch}-gogs"

            # 添加Gogs远程仓库
            remote_list = self.run_git_command(repo_path, ["remote", "show"])
            if "gogs" not in remote_list:
                self.run_git_command(repo_path, ["remote", "add", "gogs", gogs_remote])
                print("已添加Gogs远程仓库")
            else:
                # 检查远程URL是否匹配
                try:
                    current_url = self.run_git_command(repo_path, ["remote", "get-url", "gogs"]).strip()
                    if current_url != gogs_remote:
                        self.run_git_command(repo_path, ["remote", "set-url", "gogs", gogs_remote])
                        print("已更新Gogs远程仓库URL")
                except subprocess.CalledProcessError:
                    # 如果获取URL失败，先添加远程仓库，避免直接设置URL报错
                    self.run_git_command(repo_path, ["remote", "add", "gogs", gogs_remote])
                    print("已重新添加Gogs远程仓库")

            # 检查远程是否有 master 分支
            try:
                remote_branches = self.run_git_command(repo_path, ["ls-remote", "--heads", gogs_remote]).strip()
                if "master" not in remote_branches:
                    print(f"远程仓库没有 master 分支，将当前分支 {current_branch} 推送到 master 分支...")
                    try:
                        self.run_git_command(repo_path, ["push", "-u", "gogs", f"{current_branch}:master"])
                        print(f"成功将 {current_branch} 分支推送到 {repo_name} 的 master 分支")
                    except subprocess.CalledProcessError as e:
                        print(f"推送 {current_branch} 分支到 master 分支失败: {e.stderr}")
            except subprocess.CalledProcessError as e:
                print(f"获取远程分支列表失败: {e.stderr}")

            # 推送当前分支
            print(f"正在推送到 {gogs_remote} 的 {current_branch} 分支...")
            try:
                self.run_git_command(repo_path, ["push", "-u", "gogs", current_branch])
                print(f"成功推送到 {repo_name} 的 {current_branch} 分支")
            except subprocess.CalledProcessError as e:
                print(f"推送 {current_branch} 分支失败: {e.stderr}")
                return

            # 创建或切换到 gogs 前缀的分支
            branches = self.run_git_command(repo_path, ["branch", "-a"]).lower()
            if gogs_branch.lower() in branches:
                self.run_git_command(repo_path, ["checkout", gogs_branch])
            else:
                self.run_git_command(repo_path, ["checkout", "-b", gogs_branch])

            # 修改 update_submodules_to_gogs 调用部分，传入当前 gogs 分支名
            self.update_submodules_to_gogs(repo_path, gogs_branch)

            # 递归推送子模块
            self.push_submodules(repo_path, current_branch)

            # 提交子模块变更（如果有）
            if self.has_uncommitted_changes(repo_path):
                try:
                    self.run_git_command(repo_path, ["add", ".gitmodules"])
                    self.run_git_command(repo_path, ["commit", "-m", "Update submodules to Gogs URLs"])
                    print("已提交子模块变更")
                except subprocess.CalledProcessError as e:
                    print(f"警告: 提交子模块变更失败，可能没有需要提交的变更。命令: {' '.join(['git'] + e.cmd)}，错误信息: {e.stderr}")

            # 推送 gogs 前缀的分支
            print(f"正在推送到 {gogs_remote} 的 {gogs_branch} 分支...")
            try:
                self.run_git_command(repo_path, ["push", "-u", "gogs", gogs_branch])
                # 验证远程仓库是否存在该分支
                remote_branches = self.run_git_command(repo_path, ["ls-remote", "--heads", gogs_remote]).strip()
                print(f"远程分支列表:\n{remote_branches}")  # 添加调试输出
                if gogs_branch in remote_branches:
                    print(f"成功推送到 {repo_name} 的 {gogs_branch} 分支")
                else:
                    print(f"警告: 虽然推送命令执行成功，但远程仓库中未找到 {gogs_branch} 分支")
            except subprocess.CalledProcessError as e:
                print(f"推送 {gogs_branch} 分支失败: {e.stderr}")
                # 尝试强制推送
                try:
                    print("尝试强制推送以确保子模块变更被包含...")
                    self.run_git_command(repo_path, ["push", "-u", "gogs", gogs_branch, "--force"])
                    remote_branches = self.run_git_command(repo_path, ["ls-remote", "--heads", gogs_remote]).strip()
                    print(f"强制推送后远程分支列表:\n{remote_branches}")  # 添加调试输出
                    if gogs_branch in remote_branches:
                        print(f"成功强制推送到 {repo_name} 的 {gogs_branch} 分支")
                    else:
                        print(f"警告: 虽然强制推送命令执行成功，但远程仓库中未找到 {gogs_branch} 分支")
                except subprocess.CalledProcessError as force_e:
                    print(f"强制推送 {gogs_branch} 分支失败: {force_e.stderr}")

            # 切换回原分支
            self.run_git_command(repo_path, ["checkout", current_branch])

        except subprocess.CalledProcessError as e:
            # 输出更详细的错误信息
            print(f"错误: 处理仓库失败，命令: {' '.join(['git'] + e.cmd)}，错误信息: {e.stderr}")
            # 尝试切换回原分支
            if 'current_branch' in locals():
                try:
                    self.run_git_command(repo_path, ["checkout", current_branch])
                except subprocess.CalledProcessError as switch_error:
                    print(f"错误: 切换回原分支 {current_branch} 失败，命令: {' '.join(['git'] + switch_error.cmd)}，错误信息: {switch_error.stderr}")

    def update_submodules_to_gogs(self, repo_path, gogs_branch):
        if not (repo_path / ".gitmodules").exists():
            print("无子模块需要更新")
            return

        print("发现子模块，正在更新URL...")

        # 保存更新前的 .gitmodules 文件内容
        gitmodules_path = repo_path / ".gitmodules"
        with open(gitmodules_path, 'r', encoding='utf-8') as f:
            before_update_content = f.read()

        # 获取子模块列表
        submodules = self.run_git_command(repo_path, ["submodule", "status"]).splitlines()
        import re  # 导入正则表达式模块
        print(f"git submodule status 输出: {submodules}")  # 添加调试输出

        for submodule in submodules:
            print(f"正在解析子模块行: {submodule}")  # 添加调试输出
            # 进一步优化正则表达式
            match = re.search(r'^\s*[\-U*]?\w+\s+(.+?)\s*\(', submodule.strip())
            if not match:
                # 尝试匹配没有括号版本信息的情况
                match = re.search(r'^\s*[\-U*]?\w+\s+(.+?)$', submodule.strip())
            if not match:
                print(f"警告: 无法从行 '{submodule}' 解析子模块路径")
                continue
            sub_path = match.group(1).strip()
            print(f"解析到的子模块路径: {sub_path}")  # 添加调试输出

            # 检查子模块路径是否在 .gitmodules 文件中存在
            try:
                self.run_git_command(
                    repo_path,
                    ["config", "--file=.gitmodules", f"submodule.{sub_path}.path"]
                )
            except subprocess.CalledProcessError:
                print(f"警告: 子模块路径 '{sub_path}' 在 .gitmodules 文件中不存在，跳过更新")
                continue

            sub_repo_name = Path(sub_path).name

            try:
                # 获取当前子模块URL
                current_url = self.run_git_command(
                    repo_path,
                    ["config", "--file=.gitmodules", f"submodule.{sub_path}.url"]
                ).strip()

                # 构造Gogs上的子模块URL，使用不带token的方法
                gogs_submodule_url = self.get_gogs_repo_url_without_token(sub_repo_name)

                # 更新子模块URL
                if current_url != gogs_submodule_url:
                    self.run_git_command(
                        repo_path,
                        ["config", "--file=.gitmodules", f"submodule.{sub_path}.url", gogs_submodule_url]
                    )
                    print(f"更新子模块 {sub_path} 的URL: {current_url} → {gogs_submodule_url}")

                    # 更新子模块
                    self.run_git_command(repo_path, ["submodule", "update", "--init", sub_path])
                else:
                    print(f"子模块 {sub_path} 的URL已经是Gogs地址")

            except subprocess.CalledProcessError as e:
                print(f"警告: 更新子模块 {sub_path} 失败: {e.stderr}")

        # 保存更新后的 .gitmodules 文件内容
        with open(gitmodules_path, 'r', encoding='utf-8') as f:
            after_update_content = f.read()

        if before_update_content != after_update_content:
            print(".gitmodules 文件已更新，更新内容如下:")
            print("更新前内容:")
            print(before_update_content)
            print("更新后内容:")
            print(after_update_content)
        else:
            print(".gitmodules 文件未更新")

    def push_submodules(self, repo_path, current_branch):
        """递归推送子模块到 Gogs"""
        if not (repo_path / ".gitmodules").exists():
            return

        # 获取子模块列表
        submodules = self.run_git_command(repo_path, ["submodule", "status"]).splitlines()
        import re
        for submodule in submodules:
            match = re.search(r'^\s*[\-U*]?\w+\s+(.+?)\s*\(', submodule.strip())
            if not match:
                match = re.search(r'^\s*[\-U*]?\w+\s+(.+?)$', submodule.strip())
            if not match:
                continue
            sub_path = match.group(1).strip()
            sub_repo_path = repo_path / sub_path
            sub_repo_name = Path(sub_path).name

            sub_current_branch = None
            try:
                # 获取子仓库当前分支名
                sub_current_branch = self.run_git_command(sub_repo_path, ["rev-parse", "--abbrev-ref", "HEAD"]).strip()
                if sub_current_branch == "HEAD":
                    # 说明处于分离头指针状态，获取 commit id
                    commit_id = self.run_git_command(sub_repo_path, ["rev-parse", "HEAD"]).strip()
                    if commit_id:
                        new_branch_name = f"undefined-branch-{commit_id[:7]}"
                        try:
                            # 创建并切换到新分支
                            self.run_git_command(sub_repo_path, ["checkout", "-b", new_branch_name])
                            sub_current_branch = new_branch_name
                            print(f"为子仓库 {sub_repo_name} 创建并切换到 {new_branch_name} 分支")
                        except subprocess.CalledProcessError as e:
                            print(f"警告: 为子仓库 {sub_repo_name} 创建 {new_branch_name} 分支失败，错误信息: {e.stderr}")
                            continue
                    else:
                        print(f"警告: 无法获取子仓库 {sub_repo_name} 的 commit id，跳过该子仓库")
                        continue
                elif not sub_current_branch:
                    # 若分支名为空，也尝试获取 commit id 创建新分支
                    commit_id = self.run_git_command(sub_repo_path, ["rev-parse", "HEAD"]).strip()
                    if commit_id:
                        new_branch_name = f"undefined-branch-{commit_id[:7]}"
                        try:
                            self.run_git_command(sub_repo_path, ["checkout", "-b", new_branch_name])
                            sub_current_branch = new_branch_name
                            print(f"为子仓库 {sub_repo_name} 创建并切换到 {new_branch_name} 分支")
                        except subprocess.CalledProcessError as e:
                            print(f"警告: 为子仓库 {sub_repo_name} 创建 {new_branch_name} 分支失败，错误信息: {e.stderr}")
                            continue
                    else:
                        print(f"警告: 无法获取子仓库 {sub_repo_name} 的当前分支名和 commit id，跳过该子仓库")
                        continue
            except subprocess.CalledProcessError as e:
                try:
                    commit_id = self.run_git_command(sub_repo_path, ["rev-parse", "HEAD"]).strip()
                    if commit_id:
                        new_branch_name = f"undefined-branch-{commit_id[:7]}"
                        try:
                            self.run_git_command(sub_repo_path, ["checkout", "-b", new_branch_name])
                            sub_current_branch = new_branch_name
                            print(f"为子仓库 {sub_repo_name} 创建并切换到 {new_branch_name} 分支")
                        except subprocess.CalledProcessError as create_e:
                            print(f"警告: 为子仓库 {sub_repo_name} 创建 {new_branch_name} 分支失败，错误信息: {create_e.stderr}")
                            continue
                    else:
                        print(f"警告: 无法获取子仓库 {sub_repo_name} 的当前分支名和 commit id，跳过该子仓库")
                        continue
                except subprocess.CalledProcessError as commit_e:
                    print(f"警告: 获取子仓库 {sub_repo_name} 的当前分支名和 commit id 失败，错误信息: {commit_e.stderr}")
                    continue

            if self.repo_exists(sub_repo_name):
                print(f"子仓库 '{sub_repo_name}' 已存在，将更新")
                self.update_repository(sub_repo_path, sub_repo_name, sub_current_branch)  # 传入实际分支名
            else:
                print(f"子仓库 '{sub_repo_name}' 不存在，将创建")
                readme_path = sub_repo_path / 'README.md'
                description = ""
                if readme_path.exists():
                    with open(readme_path, 'r', encoding='utf-8') as f:
                        description = f.read().splitlines()[0][:100]
                self.create_and_push_repository(sub_repo_path, sub_repo_name, description, sub_current_branch)  # 传入实际分支名

    def has_uncommitted_changes(self, repo_path):
        """检查仓库是否有未提交的变更"""
        try:
            status = self.run_git_command(repo_path, ["status", "--porcelain"]).strip()
            print(f"仓库 {repo_path} 的未提交变更状态: {status}")
            return len(status) > 0
        except subprocess.CalledProcessError as e:
            print(f"检查仓库 {repo_path} 未提交变更时出错: {e.stderr}")
            return False

    def get_gogs_repo_url(self, repo_name):
        """获取包含访问令牌的Gogs仓库的URL"""
        # 将访问令牌嵌入到URL中
        return f"{self.gogs_url.replace('://', f'://{self.token}@')}/{self.gogs_username}/{repo_name}.git"

    def get_gogs_repo_url_without_token(self, repo_name):
        """获取不包含访问令牌的Gogs仓库的URL"""
        return f"{self.gogs_url}/{self.gogs_username}/{repo_name}.git"
#  python3 gogs_backup.py --gogs-url http://127.0.0.1:3000/ --token 3918dabde24c25af8b834fb0ba8eb7753f3ed405 --source-dir /home/eyyhappy/esp32_halow/voyager2/esp32
#  python gogs_backup.py --gogs-url http://127.0.0.1:3000/ --token 3918dabde24c25af8b834fb0ba8eb7753f3ed405 --source-dir C:\work\AllRepo\3.wifi_halow\wifi_halow_sdk7\wifi_halow_sdk

# git submodule update --init --recursive
# git submodule sync --recursive  # 递归同步所有子模块

def main():
    parser = argparse.ArgumentParser(description="批量备份Git仓库到Gogs并处理子模块")
    parser.add_argument("--gogs-url", default="http://127.0.0.1:3000/", help="Gogs服务器URL")
    parser.add_argument("--token", default="3918dabde24c25af8b834fb0ba8eb7753f3ed405", help="Gogs访问令牌")
    parser.add_argument("--source-dir", default=".", help="包含Git仓库的源目录")
    parser.add_argument("--dry-run", action="store_true", help="执行干跑模式，不实际创建或推送")

    args = parser.parse_args()

    tool = GogsBackupTool(
        gogs_url=args.gogs_url,
        token=args.token,
        source_dir=args.source_dir,
        dry_run=args.dry_run
    )
    tool.run()

if __name__ == "__main__":
    main()